/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.oauth2.client.oidc.authentication;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.util.Assert;

/**
 * An implementation of an {@link AuthenticationProvider} for the OpenID Connect Core 1.0
 * Authorization Code Grant Flow.
 * OpenID Connect Core 1.0授权码授予流程的一个实现
 * <p>
 * This {@link AuthenticationProvider} is responsible for authenticating an Authorization
 * Code credential with the Authorization Server's Token Endpoint and if valid, exchanging
 * it for an Access Token credential.
 * 它负责使用授权服务器的令牌端点对授权码凭证进行身份验证，如果有效，则将其交换为访问令牌凭证。
 * <p>
 * It will also obtain the user attributes of the End-User (Resource Owner) from the
 * UserInfo Endpoint using an {@link OAuth2UserService}, which will create a
 * {@code Principal} in the form of an {@link OidcUser}. The {@code OidcUser} is then
 * associated to the {@link OAuth2LoginAuthenticationToken} to complete the
 * authentication.
 * EU：End User：一个人类用户。
 * RP：Relying Party ,用来代指OAuth2中的受信任的客户端，身份认证和授权信息的消费方；
 * OP：OpenID Provider，有能力提供EU认证的服务（比如OAuth2中的授权服务），用来为RP提供EU的身份认证信息；
 * ID Token：JWT格式的数据，包含EU身份认证的信息。
 * UserInfo Endpoint：用户信息接口（受OAuth2保护），当RP使用Access Token访问时，返回授权用户的信息，此接口必须使用HTTPS
 * @author Joe Grandja
 * @author Mark Heckler
 * @since 5.0
 * @see OAuth2LoginAuthenticationToken
 * @see OAuth2AccessTokenResponseClient
 * @see OidcUserService
 * @see OidcUser
 * @see OidcIdTokenDecoderFactory
 * @see <a target="_blank" href=
 * "https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth">Section 3.1
 * Authorization Code Grant Flow</a>
 * 授权码流将授权码返回给客户端，然后客户端可以直接将其交换为ID令牌和访问令牌。这样做的好处是不会将任何令牌暴露给用户代理以及可能访问用户代理的其他恶意应用程序。
 * 授权服务器还可以在将授权码交换为访问令牌之前对客户端进行身份验证。授权码流适用于能够在自己和授权服务器之间安全地维护客户端秘密的客户端
 * @see <a target="_blank" href=
 * "https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest">Section 3.1.3.1
 * Token Request</a>
 * @see <a target="_blank" href=
 * "https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse">Section 3.1.3.3
 * Token Response</a>
 */
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {

	private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";

	private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";

	private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";

	private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;

	private final OAuth2UserService<OidcUserRequest, OidcUser> userService;

	private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();

	private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);

	/**
	 * Constructs an {@code OidcAuthorizationCodeAuthenticationProvider} using the
	 * provided parameters.
	 * @param accessTokenResponseClient the client used for requesting the access token
	 * credential from the Token Endpoint
	 * @param userService the service used for obtaining the user attributes of the
	 * End-User from the UserInfo Endpoint
	 */
	public OidcAuthorizationCodeAuthenticationProvider(
			OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
			OAuth2UserService<OidcUserRequest, OidcUser> userService) {
		Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
		Assert.notNull(userService, "userService cannot be null");
		this.accessTokenResponseClient = accessTokenResponseClient;
		this.userService = userService;
	}
    // OpenID Connect 1.0是在OAuth 2.0(授权协议)的基础上增加了身份验证层和用户信息获取功能的协议。
	// 它通过引入ID Token和用户信息端点等机制，为客户端应用程序提供了更丰富的用户身份验证和属性信息获取能力
	// 可参考：https://www.cnblogs.com/bigjor/p/16252720.html
	// 现在有两个请求A，B,分别访问授权服务器，会获取到codeA,access_tokenA以及codeB,access_tokenB,
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
		// Section 3.1.2.1 Authentication Request -
		// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
		// scope
		// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
		// 验证scope中是否有openid，没有就不是oidc认证
		if (!authorizationCodeAuthentication.getAuthorizationExchange()
			.getAuthorizationRequest()
			.getScopes()
			.contains(OidcScopes.OPENID)) {
			// This is NOT an OpenID Connect Authentication Request so return null
			// and let OAuth2LoginAuthenticationProvider handle it instead
			return null;
		}
		// 客户端发起授权请求时存储在session中的最原始授权请求信息
		OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
			.getAuthorizationRequest();
		// 授权服务器生成code后返回给客户端的响应信息（code,state）
		OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange()
			.getAuthorizationResponse();
		//授权服务器响应请求是否返回错误异常
		if (authorizationResponse.statusError()) {
			throw new OAuth2AuthenticationException(authorizationResponse.getError(),
					authorizationResponse.getError().toString());
		}
		//授权服务器返回的state和存储的state进行比对，防止CSRF攻击
		if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		// 拿着code通过远程调用的方式去授权服务器获取accessToken和idToken(该token中的cliams属性中含有一个sub,
		// 该字段是生成token用户在授权端的唯一标志，在之后通过accessToken获取用户信息后会跟该属性进行比较，从而达到身份信息的认证)
		OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);
		// 获取客户端配置信息
		ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
		// 获取授权服务器响应数据中的additionalParameters属性数据，再openId模式下该属性中应该包含一个键值对，key为id_token(进行身份认证)
		Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
		// 校验返回的参数中是否有idToken
		if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) {
			OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE,
					"Missing (required) ID Token in Token Response for Client Registration: "
							+ clientRegistration.getRegistrationId(),
					null);
			throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
		}
		// 这里对返回的idToken的jwt进行解码，校验，解码公钥来自授权服务器(通过jwk_uri获取)
		// ID Token是一个安全令牌，是一个授权服务器提供的包含用户信息（由一组Cliams构成以及其他辅助的Cliams）的JWT格式的数据结构
		OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
		// session中存储的nonce值(只能使用一次的数字)和id_token中包含的nonce值进行比较验证，以避免重放攻击  跟state防止CSRF攻击类似
		validateNonce(authorizationRequest, idToken);
		// 拿着accessToken等信息访问userinfo_uri获取userinfo,同时使用idToken对返回的用户信息进行校验,保证获取的用户信息和颁发token的用户是同一个，即身份验证
		// this.userService默认OidcUserService config中配置的
		OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
				accessTokenResponse.getAccessToken(), idToken, additionalParameters));
		//授予的权限
		Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
			.mapAuthorities(oidcUser.getAuthorities());
		//将用户信息，accessToken等信息封装成最终返回结果 注意这里将authenticated属性设置为了true,代表认证成功
		OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
				authorizationCodeAuthentication.getClientRegistration(),
				authorizationCodeAuthentication.getAuthorizationExchange(), oidcUser, mappedAuthorities,
				accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken());
		//将WebAuthenticationDetails对象设置到OAuth2LoginAuthenticationToken，里面封装了ip地址和sessionId
		authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
		return authenticationResult;
	}

	private OAuth2AccessTokenResponse getResponse(OAuth2LoginAuthenticationToken authorizationCodeAuthentication) {
		try {
			//将ClientRegistration和OAuth2AuthorizationExchange封装到OAuth2AuthorizationCodeGrantRequest对象中，而this.accessTokenResponseClient
			//的值是在OAuth2LoginConfigurer中初始化默认为DefaultAuthorizationCodeTokenResponseClient
			return this.accessTokenResponseClient.getTokenResponse(
					new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),
							authorizationCodeAuthentication.getAuthorizationExchange()));
		}
		catch (OAuth2AuthorizationException ex) {
			OAuth2Error oauth2Error = ex.getError();
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
		}
	}

	private void validateNonce(OAuth2AuthorizationRequest authorizationRequest, OidcIdToken idToken) {
		//OAuth2AuthorizationRequest再构建时如果范围包括openid，会通过随机数生成该值，存放到attributes属性中
		String requestNonce = authorizationRequest.getAttribute(OidcParameterNames.NONCE);
		if (requestNonce == null) {
			return;
		}
		//将原始值进行hash,跟idToken进行比较
		String nonceHash = getNonceHash(requestNonce);
		String nonceHashClaim = idToken.getNonce();
		if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
	}

	private String getNonceHash(String requestNonce) {
		try {
			return createHash(requestNonce);
		}
		catch (NoSuchAlgorithmException ex) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
	}

	/**
	 * Sets the {@link JwtDecoderFactory} used for {@link OidcIdToken} signature
	 * verification. The factory returns a {@link JwtDecoder} associated to the provided
	 * {@link ClientRegistration}.
	 * @param jwtDecoderFactory the {@link JwtDecoderFactory} used for {@link OidcIdToken}
	 * signature verification
	 * @since 5.2
	 */
	public final void setJwtDecoderFactory(JwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
		Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
		this.jwtDecoderFactory = jwtDecoderFactory;
	}

	/**
	 * Sets the {@link GrantedAuthoritiesMapper} used for mapping
	 * {@link OidcUser#getAuthorities()}} to a new set of authorities which will be
	 * associated to the {@link OAuth2LoginAuthenticationToken}.
	 * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
	 * user's authorities
	 */
	public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
		Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
		this.authoritiesMapper = authoritiesMapper;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
	}

	private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
			OAuth2AccessTokenResponse accessTokenResponse) {
		// 访问授权服务器获取公钥信息(JWK)以创建和初始化NimbusJwtDecoder(对jwt解码和验证)实例 即创建解码器
		JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
		//decode方法底层调用parse(token)解出明文成JWT,同时通过validateJwt方法验证解析出来的JWT
		Jwt jwt = getJwt(accessTokenResponse, jwtDecoder);
		//将解码的jwt(id_token)中的属性信息封装成OidcIdToken对象返回
		OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
				jwt.getClaims());
		return idToken;
	}

	private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) {
		try {
			Map<String, Object> parameters = accessTokenResponse.getAdditionalParameters();
			return jwtDecoder.decode((String) parameters.get(OidcParameterNames.ID_TOKEN));
		}
		catch (JwtException ex) {
			OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null);
			throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
		}
	}

	static String createHash(String nonce) throws NoSuchAlgorithmException {
		MessageDigest md = MessageDigest.getInstance("SHA-256");
		byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII));
		return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
	}

}
